--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit b27028a7fbdb9530f658ad1661824944e16e70bf
Parents : 3547991
Author : Ivan <e46112d44649266d71fe2193e00a4710>
Signature : T66BB85Valid, signed by author
Date : 2026-07-22T15:43:45-05:00
feat: implement optional seccomp-BPF syscall denylist alongside Landlock and improve tools/UI
Changes
25 files changed, 831 insertions(+), 275 deletions(-)
Diff
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d3b846ef..3f6f09ea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -32,6 +32,7 @@ All notable changes to this project will be documented in this file.
- Post-install prompts for existing users after upgrades
- Coolify-oriented Docker Compose with resource limits for deployments
- LXST telephony half-duplex mode, live duplex switching, push-to-talk (packetizer squelch), and richer in-call stats (rates plus mute state on the active call)
+- Optional Linux seccomp-BPF syscall denylist (libseccomp) alongside Landlock, with auto-detect and `MESHCHAT_SECCOMP` fallback
### Changed
diff --git a/docs/agents/skills/landlock-sqlite/SKILL.md b/docs/agents/skills/landlock-sqlite/SKILL.md
index 3651f37b..fe7a76de 100644
--- a/docs/agents/skills/landlock-sqlite/SKILL.md
+++ b/docs/agents/skills/landlock-sqlite/SKILL.md
@@ -40,4 +40,5 @@ For live stress, run Landlock in a **subprocess** (sandbox applies once per proc
- `meshchatx/src/backend/memory_pressure.py`
- `meshchatx/src/backend/message_handler.py`
- `meshchatx/src/backend/landlock_sandbox.py`
+- `meshchatx/src/backend/seccomp_sandbox.py` (syscall denylist after Landlock)
- `meshchatx/meshchat.py` (conversations/notifications error mapping)
diff --git a/docs/en/identity-and-security.md b/docs/en/identity-and-security.md
index 7463ab7f..4dc85043 100644
--- a/docs/en/identity-and-security.md
+++ b/docs/en/identity-and-security.md
@@ -57,7 +57,19 @@ Privacy mode does not disable Reticulum mesh traffic. It limits clearnet fetches
## Linux sandboxing
-Optional Landlock sandboxing on Linux restricts filesystem access for the backend. See **Linux sandboxing** in Platform guides for Firejail and Bubblewrap examples.
+On Linux, MeshChatX can enable two complementary in-process sandboxes when supported:
+
+- **Landlock** restricts filesystem paths the backend may use
+- **Seccomp-BPF** installs a syscall denylist (via libseccomp) that blocks kernel-admin and related calls a mesh client does not need
+
+Both auto-enable when available and fall back to a no-op when the platform, kernel, or libraries cannot support them. Override with:
+
+- `MESHCHAT_LANDLOCK=0` or `1`
+- `MESHCHAT_SECCOMP=0` or `1`
+
+Android never enables these in-process sandboxes (the Android app seccomp policy already constrains the process, and Landlock syscalls are blocked there).
+
+See **Linux sandboxing** in Platform guides for optional Firejail and Bubblewrap wrappers around the host install.
## Blocking and filtering
diff --git a/docs/en/platform-guides/linux-sandbox.md b/docs/en/platform-guides/linux-sandbox.md
index 52a0b21c..294c1ec3 100644
--- a/docs/en/platform-guides/linux-sandbox.md
+++ b/docs/en/platform-guides/linux-sandbox.md
@@ -4,6 +4,13 @@ This page shows how to run **`meshchatx`** under **Firejail** or **Bubblewrap**
These tools do **not** replace a full virtual machine or hardware-enforced boundary. They reduce exposure of your home directory and other paths the process can write to, when you configure them with tight whitelists or bind mounts.
+MeshChatX also applies optional **in-process** Linux sandboxes when available:
+
+- **Landlock** for filesystem path rules (`MESHCHAT_LANDLOCK=0` to disable)
+- **Seccomp-BPF** syscall denylist via libseccomp (`MESHCHAT_SECCOMP=0` to disable)
+
+Those layers fall back cleanly when unsupported. Firejail and Bubblewrap remain useful as an outer wrapper.
+
**Containers:** If you already run MeshChatX with Docker or Podman, that is a different isolation model, this document is aimed at **host-installed** `meshchatx` (or `meshchat`).
## Prerequisites
diff --git a/meshchatx.rsm b/meshchatx.rsm
index d58153f5..f30515c8 100644
Binary files a/meshchatx.rsm and b/meshchatx.rsm differ
diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index 5fcf07ca..a805affc 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -105,6 +105,13 @@ from meshchatx.src.backend.landlock_sandbox import (
landlock_kernel_supported,
landlock_requested,
)
+from meshchatx.src.backend.seccomp_sandbox import (
+ apply_seccomp_sandbox,
+ seccomp_auto_enabled,
+ seccomp_disabled_by_env,
+ seccomp_kernel_supported,
+ seccomp_requested,
+)
from meshchatx.src.backend.legacy_migrator import (
assert_migration_context_paths,
fresh_storage_at_target,
@@ -508,6 +515,7 @@ class ReticulumMeshChat:
self.listen_port: int | None = None
self.use_https: bool = True
self.landlock_active: bool = False
+ self.seccomp_active: bool = False
self._pending_identity = identity
self._network_setup_lock = threading.Lock()
self._network_ready_event = threading.Event()
@@ -4912,6 +4920,11 @@ class ReticulumMeshChat:
"landlock_auto_enabled": landlock_auto_enabled(),
"landlock_disabled_by_env": landlock_disabled_by_env(),
"landlock_active": self.landlock_active,
+ "seccomp_kernel_supported": seccomp_kernel_supported(),
+ "seccomp_requested": seccomp_requested(),
+ "seccomp_auto_enabled": seccomp_auto_enabled(),
+ "seccomp_disabled_by_env": seccomp_disabled_by_env(),
+ "seccomp_active": self.seccomp_active,
}
def get_routes(self):
@@ -25036,6 +25049,8 @@ def main():
public_dir=reticulum_meshchat.public_dir_override or get_file_path("public"),
log_dir=resolve_log_dir(),
)
+ # Apply after Landlock so landlock_* syscalls are not blocked by the filter.
+ reticulum_meshchat.seccomp_active = apply_seccomp_sandbox()
reticulum_meshchat.run(
args.host,
args.port,
diff --git a/meshchatx/src/backend/seccomp_sandbox.py b/meshchatx/src/backend/seccomp_sandbox.py
new file mode 100644
index 00000000..516da5dd
--- /dev/null
+++ b/meshchatx/src/backend/seccomp_sandbox.py
@@ -0,0 +1,268 @@
+# SPDX-License-Identifier: 0BSD
+
+"""Optional seccomp-BPF syscall denylist for the backend (Linux only).
+
+Complements Landlock filesystem rules. Default action is ALLOW with a small
+denylist of kernel-admin and process-introspection syscalls a mesh client never
+needs. When libseccomp or the kernel filter is unavailable, apply falls back to
+a no-op so the process still starts.
+"""
+
+from __future__ import annotations
+
+import ctypes
+import ctypes.util
+import errno
+import logging
+import os
+import sys
+
+logger = logging.getLogger("meshchatx.seccomp")
+
+# From linux/seccomp.h / libseccomp.h
+_SCMP_ACT_ALLOW = 0x7FFF0000
+_SCMP_ACT_ERRNO_BASE = 0x00050000
+
+# Dangerous or unused by MeshChatX. Names are resolved via libseccomp so unknown
+# names on older kernels are skipped instead of failing the whole filter.
+_DENIED_SYSCALLS = (
+ "mount",
+ "umount",
+ "umount2",
+ "pivot_root",
+ "reboot",
+ "kexec_load",
+ "kexec_file_load",
+ "init_module",
+ "finit_module",
+ "delete_module",
+ "swapon",
+ "swapoff",
+ "bpf",
+ "userfaultfd",
+ "open_by_handle_at",
+ "name_to_handle_at",
+ "setns",
+ "unshare",
+ "acct",
+ "ioperm",
+ "iopl",
+ "perf_event_open",
+ "ptrace",
+ "process_vm_readv",
+ "process_vm_writev",
+ "syslog",
+ "quotactl",
+ "lookup_dcookie",
+ "fanotify_init",
+ "open_tree",
+ "move_mount",
+ "fsopen",
+ "fsconfig",
+ "fsmount",
+ "fspick",
+ "mount_setattr",
+ "vhangup",
+ "uselib",
+ "create_module",
+ "query_module",
+ "get_kernel_syms",
+ "nfsservctl",
+ "vm86",
+ "vm86old",
+ "_sysctl",
+ "modify_ldt",
+)
+
+
+def _seccomp_env_override() -> bool | None:
+ raw = os.environ.get("MESHCHAT_SECCOMP")
+ if raw is None:
+ return None
+ val = raw.strip().lower()
+ if val in ("false", "0", "no", "off"):
+ return False
+ if val in ("true", "1", "yes", "on"):
+ return True
+ return None
+
+
+def _is_android() -> bool:
+ return hasattr(sys, "getandroidapilevel")
+
+
+_seccomp_support_cached: bool | None = None
+_seccomp_lib_cached = None
+_seccomp_lib_failed = False
+
+
+def _load_libseccomp():
+ """Return a loaded libseccomp CDLL, or None when unavailable."""
+ global _seccomp_lib_cached, _seccomp_lib_failed
+ if _seccomp_lib_failed:
+ return None
+ if _seccomp_lib_cached is not None:
+ return _seccomp_lib_cached
+
+ candidates: list[str | None] = [
+ ctypes.util.find_library("seccomp"),
+ "libseccomp.so.2",
+ "libseccomp.so",
+ ]
+ for name in candidates:
+ if not name:
+ continue
+ try:
+ lib = ctypes.CDLL(name)
+ except OSError:
+ continue
+ required = (
+ "seccomp_init",
+ "seccomp_rule_add",
+ "seccomp_load",
+ "seccomp_release",
+ "seccomp_syscall_resolve_name",
+ )
+ if any(not hasattr(lib, attr) for attr in required):
+ continue
+
+ lib.seccomp_init.argtypes = [ctypes.c_uint32]
+ lib.seccomp_init.restype = ctypes.c_void_p
+ lib.seccomp_rule_add.argtypes = [
+ ctypes.c_void_p,
+ ctypes.c_uint32,
+ ctypes.c_int,
+ ctypes.c_uint,
+ ]
+ lib.seccomp_rule_add.restype = ctypes.c_int
+ lib.seccomp_load.argtypes = [ctypes.c_void_p]
+ lib.seccomp_load.restype = ctypes.c_int
+ lib.seccomp_release.argtypes = [ctypes.c_void_p]
+ lib.seccomp_release.restype = None
+ lib.seccomp_syscall_resolve_name.argtypes = [ctypes.c_char_p]
+ lib.seccomp_syscall_resolve_name.restype = ctypes.c_int
+
+ _seccomp_lib_cached = lib
+ return lib
+
+ _seccomp_lib_failed = True
+ return None
+
+
+def _act_errno(code: int = errno.EPERM) -> int:
+ return _SCMP_ACT_ERRNO_BASE | (int(code) & 0xFFFF)
+
+
+def seccomp_library_available() -> bool:
+ """Return True when libseccomp can be loaded with the symbols we need."""
+ return _load_libseccomp() is not None
+
+
+def seccomp_kernel_supported() -> bool:
+ """Return True when this host can install a user seccomp-BPF filter."""
+ global _seccomp_support_cached
+ if _seccomp_support_cached is not None:
+ return _seccomp_support_cached
+ if sys.platform != "linux" or _is_android():
+ _seccomp_support_cached = False
+ return False
+ if not seccomp_library_available():
+ _seccomp_support_cached = False
+ return False
+ # Probe by building an empty ALLOW filter without loading it.
+ lib = _load_libseccomp()
+ if lib is None:
+ _seccomp_support_cached = False
+ return False
+ ctx = lib.seccomp_init(_SCMP_ACT_ALLOW)
+ if not ctx:
+ _seccomp_support_cached = False
+ return False
+ lib.seccomp_release(ctx)
+ _seccomp_support_cached = True
+ return True
+
+
+def seccomp_requested() -> bool:
+ if sys.platform != "linux" or _is_android():
+ return False
+ override = _seccomp_env_override()
+ if override is False:
+ return False
+ if override is True:
+ return True
+ return seccomp_kernel_supported()
+
+
+def seccomp_auto_enabled() -> bool:
+ return seccomp_requested() and _seccomp_env_override() is None
+
+
+def seccomp_disabled_by_env() -> bool:
+ return _seccomp_env_override() is False
+
+
+def apply_seccomp_sandbox() -> bool:
+ """Install the denylist filter. Returns True when seccomp-BPF is active.
+
+ Falls back to False (no filter) when unsupported, forced off, or install
+ fails. Never raises into the caller for probe or load failures.
+ """
+ if not seccomp_requested():
+ return False
+
+ lib = _load_libseccomp()
+ if lib is None:
+ logger.warning(
+ "Seccomp requested but libseccomp is unavailable; continuing without it",
+ )
+ return False
+
+ ctx = lib.seccomp_init(_SCMP_ACT_ALLOW)
+ if not ctx:
+ logger.warning("Seccomp disabled: seccomp_init failed")
+ return False
+
+ denied = 0
+ loaded = False
+ try:
+ for name in _DENIED_SYSCALLS:
+ nr = lib.seccomp_syscall_resolve_name(name.encode("ascii"))
+ if nr < 0:
+ continue
+ rc = lib.seccomp_rule_add(ctx, _act_errno(errno.EPERM), nr, 0)
+ if rc != 0:
+ logger.debug(
+ "Seccomp skip rule for %s: libseccomp rc %s",
+ name,
+ rc,
+ )
+ continue
+ denied += 1
+
+ if denied == 0:
+ logger.warning("Seccomp disabled: no denylist rules could be installed")
+ return False
+
+ rc = lib.seccomp_load(ctx)
+ if rc != 0:
+ logger.warning("Seccomp disabled: seccomp_load failed with rc %s", rc)
+ return False
+ loaded = True
+ except Exception as exc:
+ logger.warning("Seccomp disabled: %s", exc)
+ return False
+ finally:
+ lib.seccomp_release(ctx)
+
+ if not loaded:
+ return False
+
+ if seccomp_auto_enabled():
+ logger.info(
+ "Seccomp-BPF syscall denylist enabled (auto-detected, %s rules)",
+ denied,
+ )
+ else:
+ logger.info("Seccomp-BPF syscall denylist enabled (%s rules)", denied)
+ return True
diff --git a/meshchatx/src/frontend/components/rnstatus/RNStatusPage.vue b/meshchatx/src/frontend/components/rnstatus/RNStatusPage.vue
index 2efc5638..2fd5916b 100644
--- a/meshchatx/src/frontend/components/rnstatus/RNStatusPage.vue
+++ b/meshchatx/src/frontend/components/rnstatus/RNStatusPage.vue
@@ -8,27 +8,32 @@
:description="$t('tools.rnstatus.description')"
:eyebrow="$t('rnprobe.network_diagnostics')"
accent="orange"
- />
- <div
- class="flex-1 overflow-y-auto w-full px-4 md:px-5 lg:px-8 py-6 pb-[max(1.5rem,env(safe-area-inset-bottom))]"
>
- <div class="space-y-4 w-full max-w-6xl mx-auto">
- <div class="glass-card space-y-5 rounded-2xl border border-slate-200/70 p-5 dark:border-zinc-700/80">
- <div class="flex flex-wrap items-center gap-2">
- <button
- type="button"
- class="primary-chip inline-flex items-center gap-2 px-4 py-2 text-sm"
- :disabled="isLoading || reloadingRns"
- @click="refreshStatus"
+ <template #actions>
+ <button
+ type="button"
+ class="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-orange-600 text-white text-sm font-medium hover:bg-orange-700 disabled:opacity-50 disabled:pointer-events-none"
+ :disabled="isLoading || reloadingRns"
+ @click="refreshStatus"
+ >
+ <MaterialDesignIcon
+ icon-name="refresh"
+ class="h-4 w-4 shrink-0"
+ :class="{ 'animate-spin-reverse': isLoading || reloadingRns }"
+ />
+ <span class="hidden sm:inline">{{
+ reloadingRns ? $t("rnstatus.reloading") : $t("rnstatus.refresh")
+ }}</span>
+ </button>
+ </template>
+ </ToolsPageHeader>
+ <div class="flex-1 overflow-y-auto overflow-x-hidden w-full px-3 sm:px-5 md:px-5 lg:px-8 py-3 sm:py-4 min-w-0">
+ <div class="space-y-0 w-full max-w-6xl xl:max-w-7xl mx-auto min-w-0">
+ <div class="w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-4 sm:py-6 space-y-3">
+ <div class="flex flex-wrap items-center gap-3">
+ <label
+ class="inline-flex cursor-pointer items-center gap-2 rounded-lg border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-3 py-2 text-sm text-gray-800 dark:text-zinc-200"
>
- <MaterialDesignIcon
- icon-name="refresh"
- class="h-4 w-4 shrink-0"
- :class="{ 'animate-spin-reverse': isLoading || reloadingRns }"
- />
- {{ reloadingRns ? $t("rnstatus.reloading") : $t("rnstatus.refresh") }}
- </button>
- <label class="secondary-chip inline-flex cursor-pointer items-center gap-2 px-4 py-2 text-sm">
<input
v-model="includeLinkStats"
type="checkbox"
@@ -51,77 +56,80 @@
</select>
</div>
</div>
+ </div>
- <div class="space-y-3 border-t border-slate-200/70 pt-4 dark:border-zinc-700/80">
- <div class="text-sm font-semibold text-gray-900 dark:text-white">
- {{ $t("rnstatus.remote_query") }}
- </div>
- <p class="text-xs text-gray-500 dark:text-zinc-500">
- {{ $t("rnstatus.remote_query_hint") }}
- </p>
- <div class="grid gap-3 lg:grid-cols-2">
- <label class="block space-y-1">
- <span class="text-xs font-medium text-gray-700 dark:text-zinc-300">{{
- $t("rnstatus.remote_transport_hash")
- }}</span>
- <input
- v-model="remoteHash"
- type="text"
- class="input-field font-mono text-xs"
- :placeholder="$t('rnstatus.remote_transport_placeholder')"
- :disabled="reloadingRns"
- />
- </label>
- <label class="block space-y-1">
- <span class="text-xs font-medium text-gray-700 dark:text-zinc-300">{{
- $t("rnstatus.remote_timeout")
- }}</span>
- <input
- v-model.number="remoteTimeout"
- type="number"
- min="1"
- step="1"
- class="input-field text-sm"
- :disabled="reloadingRns"
- />
- </label>
- </div>
- <ManagementIdentityPicker v-model="identityPath" :disabled="reloadingRns" default-name="mgmt" />
- <div v-if="activeRemoteHash" class="flex flex-wrap items-center gap-2 text-xs">
- <span class="font-mono text-amber-700 dark:text-amber-300">{{
- $t("rnstatus.remote_active", { hash: activeRemoteHash })
+ <div class="w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-4 sm:py-6 space-y-3">
+ <h2 class="text-sm font-semibold text-gray-900 dark:text-white">
+ {{ $t("rnstatus.remote_query") }}
+ </h2>
+ <p class="text-xs text-gray-500 dark:text-zinc-500">
+ {{ $t("rnstatus.remote_query_hint") }}
+ </p>
+ <div class="grid gap-3 lg:grid-cols-2">
+ <label class="block space-y-1">
+ <span class="text-xs font-medium text-gray-700 dark:text-zinc-300">{{
+ $t("rnstatus.remote_transport_hash")
}}</span>
- <button type="button" class="secondary-chip px-2 py-1 text-xs" @click="clearRemote">
- {{ $t("rnstatus.use_local") }}
- </button>
- </div>
+ <input
+ v-model="remoteHash"
+ type="text"
+ class="input-field font-mono text-xs"
+ :placeholder="$t('rnstatus.remote_transport_placeholder')"
+ :disabled="reloadingRns"
+ />
+ </label>
+ <label class="block space-y-1">
+ <span class="text-xs font-medium text-gray-700 dark:text-zinc-300">{{
+ $t("rnstatus.remote_timeout")
+ }}</span>
+ <input
+ v-model.number="remoteTimeout"
+ type="number"
+ min="1"
+ step="1"
+ class="input-field text-sm"
+ :disabled="reloadingRns"
+ />
+ </label>
+ </div>
+ <ManagementIdentityPicker v-model="identityPath" :disabled="reloadingRns" default-name="mgmt" />
+ <div v-if="activeRemoteHash" class="flex flex-wrap items-center gap-2 text-xs">
+ <span class="font-mono text-amber-700 dark:text-amber-300">{{
+ $t("rnstatus.remote_active", { hash: activeRemoteHash })
+ }}</span>
+ <button
+ type="button"
+ class="inline-flex items-center px-2 py-1 rounded-lg border border-gray-300 dark:border-zinc-600 bg-white dark:bg-zinc-900 text-xs font-medium text-gray-800 dark:text-zinc-200 hover:bg-gray-50 dark:hover:bg-zinc-800"
+ @click="clearRemote"
+ >
+ {{ $t("rnstatus.use_local") }}
+ </button>
</div>
+ </div>
- <div class="grid grid-cols-1 gap-3 lg:grid-cols-2">
+ <div
+ v-if="linkCount !== null || blackholeEnabled !== null"
+ class="w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-4 sm:py-6"
+ >
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div
v-if="linkCount !== null"
- class="rounded-xl border border-blue-200/80 bg-blue-50/90 p-4 text-blue-800 dark:border-blue-800/50 dark:bg-blue-950/30 dark:text-blue-200"
+ class="text-sm font-semibold text-gray-900 dark:text-white tabular-nums"
>
- <div class="text-sm font-semibold">
- {{ $t("rnstatus.active_links", { count: formatInt(linkCount) }) }}
- </div>
+ {{ $t("rnstatus.active_links", { count: formatInt(linkCount) }) }}
</div>
-
- <div
- v-if="blackholeEnabled !== null"
- class="rounded-xl border border-purple-200/80 bg-purple-50/90 p-4 text-purple-900 dark:border-purple-800/50 dark:bg-purple-950/30 dark:text-purple-100"
- >
- <div class="flex flex-wrap items-center justify-between gap-2 font-semibold">
- <span>{{
+ <div v-if="blackholeEnabled !== null" class="space-y-0.5">
+ <div class="text-sm font-semibold text-gray-900 dark:text-white">
+ {{
$t("rnstatus.blackhole_label", {
state: blackholeEnabled
? $t("rnstatus.blackhole_publishing")
: $t("rnstatus.blackhole_inactive"),
})
- }}</span>
- <span class="text-sm font-normal opacity-90">
- {{ formatInt(blackholeCount) }} Identities
- </span>
+ }}
+ </div>
+ <div class="text-xs text-gray-500 dark:text-zinc-500 tabular-nums">
+ {{ formatInt(blackholeCount) }} Identities
</div>
</div>
</div>
@@ -129,14 +137,14 @@
<div
v-if="blackholeSources.length > 0"
- class="glass-card space-y-3 rounded-2xl border border-slate-200/70 p-5 dark:border-zinc-700/80"
+ class="w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-4 sm:py-6 space-y-3"
>
- <div class="font-semibold text-lg text-gray-900 dark:text-white">Blackhole Sources</div>
- <div class="grid gap-2">
+ <h2 class="text-sm font-semibold text-gray-900 dark:text-white">Blackhole Sources</h2>
+ <div class="divide-y divide-gray-100 dark:divide-zinc-800/50">
<div
v-for="source in blackholeSources"
:key="source"
- class="text-sm font-mono bg-gray-50 dark:bg-gray-800 p-2 rounded-sm truncate"
+ class="py-2 text-sm font-mono text-gray-800 dark:text-zinc-200 truncate"
>
{{ source }}
</div>
@@ -145,145 +153,161 @@
<div
v-if="interfaces.length === 0 && !isLoading && !reloadingRns"
- class="glass-card p-8 text-center text-gray-500 dark:text-gray-400"
+ class="w-full py-8 sm:py-12 text-center text-sm text-gray-500 dark:text-gray-400"
>
{{ $t("rnstatus.no_interfaces_found") }}
</div>
- <div
- v-for="iface in interfaces"
- :key="iface.name"
- class="glass-card overflow-hidden rounded-2xl border border-slate-200/70 dark:border-zinc-700/80"
- >
- <div
- class="flex flex-wrap items-start justify-between gap-3 border-b border-slate-100 bg-slate-50/60 px-4 py-4 dark:border-zinc-800/80 dark:bg-zinc-900/40 sm:px-5"
- >
- <div class="min-w-0 flex-1 space-y-2">
- <div class="flex flex-wrap items-center gap-2">
- <h3
- class="wrap-break-word text-base font-semibold leading-snug text-gray-900 dark:text-white sm:text-lg"
- >
- {{ iface.name }}
- </h3>
- <span
- v-if="iface.discovered"
- class="inline-flex shrink-0 items-center rounded-md bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-900 dark:bg-amber-900/45 dark:text-amber-100"
- >
- Discovered
- </span>
+ <div v-else class="w-full divide-y divide-gray-200/60 dark:divide-zinc-800/60">
+ <div v-for="iface in interfaces" :key="iface.name" class="py-4 sm:py-6 space-y-4">
+ <div class="flex flex-wrap items-start justify-between gap-3">
+ <div class="min-w-0 flex-1 space-y-1">
+ <div class="flex flex-wrap items-center gap-2">
+ <h3
+ class="wrap-break-word text-base font-semibold leading-snug text-gray-900 dark:text-white"
+ >
+ {{ iface.name }}
+ </h3>
+ <span
+ v-if="iface.discovered"
+ class="inline-flex shrink-0 items-center rounded-md bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-900 dark:bg-amber-900/45 dark:text-amber-100"
+ >
+ Discovered
+ </span>
+ </div>
</div>
+ <span
+ :class="[
+ iface.status === 'Up'
+ ? 'bg-green-100 text-green-800 dark:bg-green-900/45 dark:text-green-100'
+ : 'bg-red-100 text-red-800 dark:bg-red-900/45 dark:text-red-100',
+ 'shrink-0 rounded-full px-3 py-1 text-xs font-semibold',
+ ]"
+ >
+ {{ iface.status }}
+ </span>
</div>
- <span
- :class="[
- iface.status === 'Up'
- ? 'bg-green-100 text-green-800 dark:bg-green-900/45 dark:text-green-100'
- : 'bg-red-100 text-red-800 dark:bg-red-900/45 dark:text-red-100',
- 'shrink-0 rounded-full px-3 py-1 text-xs font-semibold',
- ]"
- >
- {{ iface.status }}
- </span>
- </div>
- <div class="grid gap-x-6 gap-y-4 p-4 text-sm sm:p-5 md:grid-cols-2 lg:grid-cols-3">
- <div v-if="iface.mode">
- <div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.mode") }}</div>
- <div class="font-semibold text-gray-900 dark:text-white">{{ iface.mode }}</div>
- </div>
- <div v-if="iface.bitrate">
- <div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.bitrate") }}</div>
- <div class="font-semibold text-gray-900 dark:text-white">{{ iface.bitrate }}</div>
- </div>
- <div v-if="iface.rx_bytes_str">
- <div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.rx_bytes") }}</div>
- <div class="font-semibold text-gray-900 dark:text-white">{{ iface.rx_bytes_str }}</div>
- </div>
- <div v-if="iface.tx_bytes_str">
- <div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.tx_bytes") }}</div>
- <div class="font-semibold text-gray-900 dark:text-white">{{ iface.tx_bytes_str }}</div>
- </div>
- <div v-if="iface.rx_packets !== undefined">
- <div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.rx_packets") }}</div>
- <div class="font-semibold tabular-nums text-gray-900 dark:text-white">
- {{ iface.rx_packets }}
+ <div class="grid gap-x-6 gap-y-3 text-sm md:grid-cols-2 lg:grid-cols-3">
+ <div v-if="iface.mode">
+ <div class="text-xs text-gray-500 dark:text-gray-400">{{ $t("rnstatus.mode") }}</div>
+ <div class="font-semibold text-gray-900 dark:text-white">{{ iface.mode }}</div>
</div>
- </div>
- <div v-if="iface.tx_packets !== undefined">
- <div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.tx_packets") }}</div>
- <div class="font-semibold tabular-nums text-gray-900 dark:text-white">
- {{ iface.tx_packets }}
+ <div v-if="iface.bitrate">
+ <div class="text-xs text-gray-500 dark:text-gray-400">{{ $t("rnstatus.bitrate") }}</div>
+ <div class="font-semibold text-gray-900 dark:text-white">{{ iface.bitrate }}</div>
</div>
- </div>
- <div v-if="iface.clients !== undefined">
- <div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.clients") }}</div>
- <div class="font-semibold text-gray-900 dark:text-white">
- {{ formatInt(iface.clients) }}
+ <div v-if="iface.rx_bytes_str">
+ <div class="text-xs text-gray-500 dark:text-gray-400">
+ {{ $t("rnstatus.rx_bytes") }}
+ </div>
+ <div class="font-semibold text-gray-900 dark:text-white">{{ iface.rx_bytes_str }}</div>
</div>
- </div>
- <div v-if="iface.peers !== undefined">
- <div class="text-gray-500 dark:text-gray-400">Peers</div>
- <div class="font-semibold text-gray-900 dark:text-white">
- {{ formatInt(iface.peers) }} {{ $t("rnstatus.peers_reachable") }}
+ <div v-if="iface.tx_bytes_str">
+ <div class="text-xs text-gray-500 dark:text-gray-400">
+ {{ $t("rnstatus.tx_bytes") }}
+ </div>
+ <div class="font-semibold text-gray-900 dark:text-white">{{ iface.tx_bytes_str }}</div>
</div>
- </div>
- <div v-if="iface.noise_floor">
- <div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.noise_floor") }}</div>
- <div class="font-semibold text-gray-900 dark:text-white">{{ iface.noise_floor }}</div>
- </div>
- <div v-if="iface.interference">
- <div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.interference") }}</div>
- <div class="font-semibold text-gray-900 dark:text-white">{{ iface.interference }}</div>
- </div>
- <div v-if="iface.cpu_load">
- <div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.cpu_load") }}</div>
- <div class="font-semibold text-gray-900 dark:text-white">{{ iface.cpu_load }}</div>
- </div>
- <div v-if="iface.cpu_temp">
- <div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.cpu_temp") }}</div>
- <div class="font-semibold text-gray-900 dark:text-white">{{ iface.cpu_temp }}</div>
- </div>
- <div v-if="iface.mem_load">
- <div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.memory_load") }}</div>
- <div class="font-semibold text-gray-900 dark:text-white">{{ iface.mem_load }}</div>
- </div>
- <div v-if="iface.battery_percent !== undefined">
- <div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.battery") }}</div>
- <div class="font-semibold text-gray-900 dark:text-white">
- {{ formatInt(iface.battery_percent) }}%<span v-if="iface.battery_state">
- ({{ iface.battery_state }})</span
- >
+ <div v-if="iface.rx_packets !== undefined">
+ <div class="text-xs text-gray-500 dark:text-gray-400">
+ {{ $t("rnstatus.rx_packets") }}
+ </div>
+ <div class="font-semibold tabular-nums text-gray-900 dark:text-white">
+ {{ iface.rx_packets }}
+ </div>
</div>
- </div>
- <div v-if="iface.network_name">
- <div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.network") }}</div>
- <div class="font-semibold text-gray-900 dark:text-white">{{ iface.network_name }}</div>
- </div>
- <div v-if="iface.incoming_announce_frequency !== undefined">
- <div class="text-gray-500 dark:text-gray-400">
- {{ $t("rnstatus.incoming_announces") }}
+ <div v-if="iface.tx_packets !== undefined">
+ <div class="text-xs text-gray-500 dark:text-gray-400">
+ {{ $t("rnstatus.tx_packets") }}
+ </div>
+ <div class="font-semibold tabular-nums text-gray-900 dark:text-white">
+ {{ iface.tx_packets }}
+ </div>
</div>
- <div class="font-semibold tabular-nums text-gray-900 dark:text-white">
- {{ iface.incoming_announce_frequency }}/s
+ <div v-if="iface.clients !== undefined">
+ <div class="text-xs text-gray-500 dark:text-gray-400">{{ $t("rnstatus.clients") }}</div>
+ <div class="font-semibold text-gray-900 dark:text-white">
+ {{ formatInt(iface.clients) }}
+ </div>
</div>
- </div>
- <div v-if="iface.outgoing_announce_frequency !== undefined">
- <div class="text-gray-500 dark:text-gray-400">
- {{ $t("rnstatus.outgoing_announces") }}
+ <div v-if="iface.peers !== undefined">
+ <div class="text-xs text-gray-500 dark:text-gray-400">Peers</div>
+ <div class="font-semibold text-gray-900 dark:text-white">
+ {{ formatInt(iface.peers) }} {{ $t("rnstatus.peers_reachable") }}
+ </div>
</div>
- <div class="font-semibold tabular-nums text-gray-900 dark:text-white">
- {{ iface.outgoing_announce_frequency }}/s
+ <div v-if="iface.noise_floor">
+ <div class="text-xs text-gray-500 dark:text-gray-400">
+ {{ $t("rnstatus.noise_floor") }}
+ </div>
+ <div class="font-semibold text-gray-900 dark:text-white">{{ iface.noise_floor }}</div>
</div>
- </div>
- <div v-if="iface.airtime">
- <div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.airtime") }}</div>
- <div class="font-semibold text-gray-900 dark:text-white">
- {{ iface.airtime.short }}% (15s), {{ iface.airtime.long }}% (1h)
+ <div v-if="iface.interference">
+ <div class="text-xs text-gray-500 dark:text-gray-400">
+ {{ $t("rnstatus.interference") }}
+ </div>
+ <div class="font-semibold text-gray-900 dark:text-white">{{ iface.interference }}</div>
</div>
- </div>
- <div v-if="iface.channel_load">
- <div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.channel_load") }}</div>
- <div class="font-semibold text-gray-900 dark:text-white">
- {{ iface.channel_load.short }}% (15s), {{ iface.channel_load.long }}% (1h)
+ <div v-if="iface.cpu_load">
+ <div class="text-xs text-gray-500 dark:text-gray-400">
+ {{ $t("rnstatus.cpu_load") }}
+ </div>
+ <div class="font-semibold text-gray-900 dark:text-white">{{ iface.cpu_load }}</div>
+ </div>
+ <div v-if="iface.cpu_temp">
+ <div class="text-xs text-gray-500 dark:text-gray-400">
+ {{ $t("rnstatus.cpu_temp") }}
+ </div>
+ <div class="font-semibold text-gray-900 dark:text-white">{{ iface.cpu_temp }}</div>
+ </div>
+ <div v-if="iface.mem_load">
+ <div class="text-xs text-gray-500 dark:text-gray-400">
+ {{ $t("rnstatus.memory_load") }}
+ </div>
+ <div class="font-semibold text-gray-900 dark:text-white">{{ iface.mem_load }}</div>
+ </div>
+ <div v-if="iface.battery_percent !== undefined">
+ <div class="text-xs text-gray-500 dark:text-gray-400">{{ $t("rnstatus.battery") }}</div>
+ <div class="font-semibold text-gray-900 dark:text-white">
+ {{ formatInt(iface.battery_percent) }}%<span v-if="iface.battery_state">
+ ({{ iface.battery_state }})</span
+ >
+ </div>
+ </div>
+ <div v-if="iface.network_name">
+ <div class="text-xs text-gray-500 dark:text-gray-400">{{ $t("rnstatus.network") }}</div>
+ <div class="font-semibold text-gray-900 dark:text-white">{{ iface.network_name }}</div>
+ </div>
+ <div v-if="iface.incoming_announce_frequency !== undefined">
+ <div class="text-xs text-gray-500 dark:text-gray-400">
+ {{ $t("rnstatus.incoming_announces") }}
+ </div>
+ <div class="font-semibold tabular-nums text-gray-900 dark:text-white">
+ {{ iface.incoming_announce_frequency }}/s
+ </div>
+ </div>
+ <div v-if="iface.outgoing_announce_frequency !== undefined">
+ <div class="text-xs text-gray-500 dark:text-gray-400">
+ {{ $t("rnstatus.outgoing_announces") }}
+ </div>
+ <div class="font-semibold tabular-nums text-gray-900 dark:text-white">
+ {{ iface.outgoing_announce_frequency }}/s
+ </div>
+ </div>
+ <div v-if="iface.airtime">
+ <div class="text-xs text-gray-500 dark:text-gray-400">{{ $t("rnstatus.airtime") }}</div>
+ <div class="font-semibold text-gray-900 dark:text-white">
+ {{ iface.airtime.short }}% (15s), {{ iface.airtime.long }}% (1h)
+ </div>
+ </div>
+ <div v-if="iface.channel_load">
+ <div class="text-xs text-gray-500 dark:text-gray-400">
+ {{ $t("rnstatus.channel_load") }}
+ </div>
+ <div class="font-semibold text-gray-900 dark:text-white">
+ {{ iface.channel_load.short }}% (15s), {{ iface.channel_load.long }}% (1h)
+ </div>
</div>
</div>
</div>
diff --git a/meshchatx/src/frontend/components/settings/SettingsPage.vue b/meshchatx/src/frontend/components/settings/SettingsPage.vue
index b68263b7..9b9da5ca 100644
--- a/meshchatx/src/frontend/components/settings/SettingsPage.vue
+++ b/meshchatx/src/frontend/components/settings/SettingsPage.vue
@@ -3164,6 +3164,23 @@
: $t("app.landlock_inactive")
}}
</div>
+ <div
+ v-if="serverSecurity.seccomp_requested !== undefined"
+ class="text-xs text-gray-600 dark:text-gray-400"
+ >
+ {{ $t("app.seccomp_status") }}:
+ {{
+ serverSecurity.seccomp_active
+ ? serverSecurity.seccomp_auto_enabled
+ ? $t("app.seccomp_auto_enabled")
+ : $t("app.seccomp_active")
+ : serverSecurity.seccomp_kernel_supported === false
+ ? $t("app.seccomp_kernel_unsupported")
+ : serverSecurity.seccomp_disabled_by_env
+ ? $t("app.seccomp_disabled_by_env")
+ : $t("app.seccomp_inactive")
+ }}
+ </div>
<div
v-if="serverSecurity.is_loopback_bind === false"
class="rounded-md border border-amber-500/40 bg-amber-500/10 p-4 space-y-3"
@@ -4050,6 +4067,11 @@ export default {
landlock_kernel_supported: false,
landlock_auto_enabled: false,
landlock_disabled_by_env: false,
+ seccomp_requested: false,
+ seccomp_active: false,
+ seccomp_kernel_supported: false,
+ seccomp_auto_enabled: false,
+ seccomp_disabled_by_env: false,
},
exposureAckFirewall: false,
exposureAckVpn: false,
diff --git a/meshchatx/src/frontend/js/registries/coreToolsEntries.js b/meshchatx/src/frontend/js/registries/coreToolsEntries.js
index a11b246a..ca036f1c 100644
--- a/meshchatx/src/frontend/js/registries/coreToolsEntries.js
+++ b/meshchatx/src/frontend/js/registries/coreToolsEntries.js
@@ -39,32 +39,6 @@ export const CORE_TOOLS_ENTRIES = [
titleKey: "tools.rnprobe.title",
descriptionKey: "tools.rnprobe.description",
},
- {
- name: "rncp",
- route: { name: "rncp" },
- icon: "swap-horizontal",
- iconBg: "tool-card__icon bg-green-50 text-green-500 dark:bg-green-900/30 dark:text-green-200",
- titleKey: "tools.rncp.title",
- descriptionKey: "tools.rncp.description",
- },
- {
- name: "rnsh",
- route: { name: "rnsh" },
- icon: "console-network-outline",
- iconBg: "tool-card__icon bg-indigo-50 text-indigo-600 dark:bg-indigo-900/30 dark:text-indigo-200",
- titleKey: "tools.rnsh.title",
- descriptionKey: "tools.rnsh.description",
- alpha: true,
- },
- {
- name: "rnx",
- route: { name: "rnx" },
- icon: "console",
- iconBg: "tool-card__icon bg-teal-50 text-teal-600 dark:bg-teal-900/30 dark:text-teal-200",
- titleKey: "tools.rnx.title",
- descriptionKey: "tools.rnx.description",
- alpha: true,
- },
{
name: "rnstatus",
route: { name: "rnstatus" },
@@ -90,20 +64,39 @@ export const CORE_TOOLS_ENTRIES = [
descriptionKey: "tools.rnpath_trace.description",
},
{
- name: "translator",
- route: { name: "translator" },
- icon: "translate",
- iconBg: "tool-card__icon bg-indigo-50 text-indigo-500 dark:bg-indigo-900/30 dark:text-indigo-200",
- titleKey: "tools.translator.title",
- descriptionKey: "tools.translator.description",
+ name: "rncp",
+ route: { name: "rncp" },
+ icon: "swap-horizontal",
+ iconBg: "tool-card__icon bg-green-50 text-green-500 dark:bg-green-900/30 dark:text-green-200",
+ titleKey: "tools.rncp.title",
+ descriptionKey: "tools.rncp.description",
},
{
- name: "bots",
- route: { name: "bots" },
- icon: "robot",
- iconBg: "tool-card__icon bg-blue-50 text-blue-500 dark:bg-blue-900/30 dark:text-blue-200",
- titleKey: "tools.bots.title",
- descriptionKey: "tools.bots.description",
+ name: "rns-filesync",
+ route: { name: "rns-filesync" },
+ icon: "folder-sync",
+ iconBg: "tool-card__icon bg-emerald-50 text-emerald-500 dark:bg-emerald-900/30 dark:text-emerald-200",
+ titleKey: "tools.rns_filesync.title",
+ descriptionKey: "tools.rns_filesync.description",
+ alpha: true,
+ },
+ {
+ name: "rnsh",
+ route: { name: "rnsh" },
+ icon: "console-network-outline",
+ iconBg: "tool-card__icon bg-indigo-50 text-indigo-600 dark:bg-indigo-900/30 dark:text-indigo-200",
+ titleKey: "tools.rnsh.title",
+ descriptionKey: "tools.rnsh.description",
+ alpha: true,
+ },
+ {
+ name: "rnx",
+ route: { name: "rnx" },
+ icon: "console",
+ iconBg: "tool-card__icon bg-teal-50 text-teal-600 dark:bg-teal-900/30 dark:text-teal-200",
+ titleKey: "tools.rnx.title",
+ descriptionKey: "tools.rnx.description",
+ alpha: true,
},
{
name: "propagation-nodes",
@@ -138,6 +131,38 @@ export const CORE_TOOLS_ENTRIES = [
descriptionKey: "tools.message_blocklist.description",
beta: true,
},
+ {
+ name: "bots",
+ route: { name: "bots" },
+ icon: "robot",
+ iconBg: "tool-card__icon bg-blue-50 text-blue-500 dark:bg-blue-900/30 dark:text-blue-200",
+ titleKey: "tools.bots.title",
+ descriptionKey: "tools.bots.description",
+ },
+ {
+ name: "paper-message",
+ route: { name: "paper-message" },
+ icon: "qrcode",
+ iconBg: "tool-card__icon bg-blue-50 text-blue-500 dark:bg-blue-900/30 dark:text-blue-200",
+ titleKey: "tools.paper_message.title",
+ descriptionKey: "tools.paper_message.description",
+ },
+ {
+ name: "translator",
+ route: { name: "translator" },
+ icon: "translate",
+ iconBg: "tool-card__icon bg-indigo-50 text-indigo-500 dark:bg-indigo-900/30 dark:text-indigo-200",
+ titleKey: "tools.translator.title",
+ descriptionKey: "tools.translator.description",
+ },
+ {
+ name: "micron-editor",
+ route: { name: "micron-editor" },
+ icon: "code-tags",
+ iconBg: "tool-card__icon bg-teal-50 text-teal-500 dark:bg-teal-900/30 dark:text-teal-200",
+ titleKey: "tools.micron_editor.title",
+ descriptionKey: "tools.micron_editor.description",
+ },
{
name: "documentation",
route: { name: "documentation" },
@@ -154,14 +179,6 @@ export const CORE_TOOLS_ENTRIES = [
titleKey: "tools.repository_server.title",
descriptionKey: "tools.repository_server.description",
},
- {
- name: "micron-editor",
- route: { name: "micron-editor" },
- icon: "code-tags",
- iconBg: "tool-card__icon bg-teal-50 text-teal-500 dark:bg-teal-900/30 dark:text-teal-200",
- titleKey: "tools.micron_editor.title",
- descriptionKey: "tools.micron_editor.description",
- },
{
name: "reticulum-config-editor",
route: { name: "reticulum-config-editor" },
@@ -170,14 +187,6 @@ export const CORE_TOOLS_ENTRIES = [
titleKey: "tools.reticulum_config_editor.title",
descriptionKey: "tools.reticulum_config_editor.description",
},
- {
- name: "paper-message",
- route: { name: "paper-message" },
- icon: "qrcode",
- iconBg: "tool-card__icon bg-blue-50 text-blue-500 dark:bg-blue-900/30 dark:text-blue-200",
- titleKey: "tools.paper_message.title",
- descriptionKey: "tools.paper_message.description",
- },
{
name: "rnode-flasher",
route: { name: "rnode-flasher" },
@@ -210,14 +219,6 @@ export const CORE_TOOLS_ENTRIES = [
titleKey: "tools.rns_tunnel.title",
descriptionKey: "tools.rns_tunnel.description",
},
- {
- name: "rns-filesync",
- route: { name: "rns-filesync" },
- icon: "folder-sync",
- iconBg: "tool-card__icon bg-emerald-50 text-emerald-500 dark:bg-emerald-900/30 dark:text-emerald-200",
- titleKey: "tools.rns_filesync.title",
- descriptionKey: "tools.rns_filesync.description",
- },
{
name: "debug-logs",
route: { name: "debug-logs" },
diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json
index 07298270..0d4d9543 100644
--- a/meshchatx/src/frontend/locales/de.json
+++ b/meshchatx/src/frontend/locales/de.json
@@ -515,7 +515,13 @@
"cancel_inbound_deliveries_count": "Eingehende abbrechen ({count})",
"cancel_inbound_confirm": "{count} eingehende LXMF-Übertragung(en) abbrechen? Große Nachrichten, die gerade heruntergeladen werden, werden gestoppt.",
"cancel_inbound_done": "{count} eingehende Übertragung(en) abgebrochen.",
- "cancel_inbound_failed": "Eingehende Übertragungen konnten nicht abgebrochen werden"
+ "cancel_inbound_failed": "Eingehende Übertragungen konnten nicht abgebrochen werden",
+ "seccomp_status": "Seccomp-BPF-Sandbox (Linux)",
+ "seccomp_active": "Aktiv",
+ "seccomp_inactive": "Nicht aktiv",
+ "seccomp_auto_enabled": "Automatisch aktiviert, wenn libseccomp verfügbar ist",
+ "seccomp_kernel_unsupported": "Seccomp-BPF nicht verfügbar (libseccomp installieren oder Plattform nicht unterstützt)",
+ "seccomp_disabled_by_env": "Deaktiviert über MESHCHAT_SECCOMP=0"
},
"common": {
"open": "Öffnen",
diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json
index ff621b06..de427826 100644
--- a/meshchatx/src/frontend/locales/en.json
+++ b/meshchatx/src/frontend/locales/en.json
@@ -224,6 +224,12 @@
"landlock_auto_enabled": "Enabled automatically on this Linux kernel",
"landlock_kernel_unsupported": "Kernel does not support Landlock (5.13+ required)",
"landlock_disabled_by_env": "Disabled via MESHCHAT_LANDLOCK=0",
+ "seccomp_status": "Seccomp-BPF sandbox (Linux)",
+ "seccomp_active": "Active",
+ "seccomp_inactive": "Not active",
+ "seccomp_auto_enabled": "Enabled automatically when libseccomp is available",
+ "seccomp_kernel_unsupported": "Seccomp-BPF unavailable (install libseccomp, or unsupported platform)",
+ "seccomp_disabled_by_env": "Disabled via MESHCHAT_SECCOMP=0",
"settings_map_eyebrow": "Map",
"messages_description": "Configure how MeshChat handles message delivery failures. Control automatic retry behavior, attachment retransmission, and fallback mechanisms to ensure reliable message delivery across the mesh network.",
"auto_resend_title": "Auto resend when peer announces",
diff --git a/meshchatx/src/frontend/locales/es.json b/meshchatx/src/frontend/locales/es.json
index 8b5035ba..58d3b04b 100644
--- a/meshchatx/src/frontend/locales/es.json
+++ b/meshchatx/src/frontend/locales/es.json
@@ -515,7 +515,13 @@
"cancel_inbound_deliveries_count": "Cancelar entrantes ({count})",
"cancel_inbound_confirm": "¿Cancelar {count} transferencia(s) entrante(s) de LXMF? Se detendrán los mensajes grandes que se estén descargando.",
"cancel_inbound_done": "Se cancelaron {count} transferencia(s) entrante(s).",
- "cancel_inbound_failed": "No se pudieron cancelar las entregas entrantes"
+ "cancel_inbound_failed": "No se pudieron cancelar las entregas entrantes",
+ "seccomp_status": "Sandbox Seccomp-BPF (Linux)",
+ "seccomp_active": "Activo",
+ "seccomp_inactive": "Inactivo",
+ "seccomp_auto_enabled": "Activado automáticamente cuando libseccomp está disponible",
+ "seccomp_kernel_unsupported": "Seccomp-BPF no disponible (instala libseccomp o plataforma no compatible)",
+ "seccomp_disabled_by_env": "Desactivado con MESHCHAT_SECCOMP=0"
},
"common": {
"open": "Abierto",
diff --git a/meshchatx/src/frontend/locales/fi.json b/meshchatx/src/frontend/locales/fi.json
index 9c9ef9ac..65c59478 100644
--- a/meshchatx/src/frontend/locales/fi.json
+++ b/meshchatx/src/frontend/locales/fi.json
@@ -515,7 +515,13 @@
"cancel_inbound_deliveries_count": "Peruuta saapuvat ({count})",
"cancel_inbound_confirm": "Perutaanko {count} saapuvaa LXMF-siirtoa? Suurten, juuri ladattavien viestien lataus pysähtyy.",
"cancel_inbound_done": "Peruttiin {count} saapuvaa siirtoa.",
- "cancel_inbound_failed": "Saapuvia siirtoja ei voitu perua"
+ "cancel_inbound_failed": "Saapuvia siirtoja ei voitu perua",
+ "seccomp_status": "Seccomp-BPF-hiekkalaatikko (Linux)",
+ "seccomp_active": "Aktiivinen",
+ "seccomp_inactive": "Ei aktiivinen",
+ "seccomp_auto_enabled": "Käytössä automaattisesti, kun libseccomp on saatavilla",
+ "seccomp_kernel_unsupported": "Seccomp-BPF ei ole käytettävissä (asenna libseccomp tai alusta ei tuettu)",
+ "seccomp_disabled_by_env": "Pois käytöstä MESHCHAT_SECCOMP=0"
},
"common": {
"open": "Avaa",
diff --git a/meshchatx/src/frontend/locales/fr.json b/meshchatx/src/frontend/locales/fr.json
index d5462a23..40df7348 100644
--- a/meshchatx/src/frontend/locales/fr.json
+++ b/meshchatx/src/frontend/locales/fr.json
@@ -515,7 +515,13 @@
"cancel_inbound_deliveries_count": "Annuler entrants ({count})",
"cancel_inbound_confirm": "Annuler {count} transfert(s) entrant(s) LXMF ? Les grands messages en cours de téléchargement seront arrêtés.",
"cancel_inbound_done": "{count} transfert(s) entrant(s) annulé(s).",
- "cancel_inbound_failed": "Échec de l'annulation des livraisons entrantes"
+ "cancel_inbound_failed": "Échec de l'annulation des livraisons entrantes",
+ "seccomp_status": "Bac à sable Seccomp-BPF (Linux)",
+ "seccomp_active": "Actif",
+ "seccomp_inactive": "Inactif",
+ "seccomp_auto_enabled": "Activé automatiquement lorsque libseccomp est disponible",
+ "seccomp_kernel_unsupported": "Seccomp-BPF indisponible (installez libseccomp, ou plateforme non prise en charge)",
+ "seccomp_disabled_by_env": "Désactivé via MESHCHAT_SECCOMP=0"
},
"common": {
"open": "Ouvrir",
diff --git a/meshchatx/src/frontend/locales/it.json b/meshchatx/src/frontend/locales/it.json
index 34c2e478..9910fa16 100644
--- a/meshchatx/src/frontend/locales/it.json
+++ b/meshchatx/src/frontend/locales/it.json
@@ -515,7 +515,13 @@
"cancel_inbound_deliveries_count": "Annulla in arrivo ({count})",
"cancel_inbound_confirm": "Annullare {count} trasferimento/i LXMF in arrivo? I messaggi grandi in download verranno interrotti.",
"cancel_inbound_done": "Annullati {count} trasferimento/i in arrivo.",
- "cancel_inbound_failed": "Impossibile annullare le consegne in arrivo"
+ "cancel_inbound_failed": "Impossibile annullare le consegne in arrivo",
+ "seccomp_status": "Sandbox Seccomp-BPF (Linux)",
+ "seccomp_active": "Attivo",
+ "seccomp_inactive": "Non attivo",
+ "seccomp_auto_enabled": "Abilitato automaticamente quando libseccomp è disponibile",
+ "seccomp_kernel_unsupported": "Seccomp-BPF non disponibile (installa libseccomp o piattaforma non supportata)",
+ "seccomp_disabled_by_env": "Disabilitato tramite MESHCHAT_SECCOMP=0"
},
"common": {
"open": "Apri",
diff --git a/meshchatx/src/frontend/locales/nl.json b/meshchatx/src/frontend/locales/nl.json
index 53d47d7a..0f14e72d 100644
--- a/meshchatx/src/frontend/locales/nl.json
+++ b/meshchatx/src/frontend/locales/nl.json
@@ -515,7 +515,13 @@
"cancel_inbound_deliveries_count": "Inkomend annuleren ({count})",
"cancel_inbound_confirm": "{count} inkomende LXMF-overdracht(en) annuleren? Grote berichten die nu worden gedownload stoppen.",
"cancel_inbound_done": "{count} inkomende overdracht(en) geannuleerd.",
- "cancel_inbound_failed": "Inkomende leveringen konden niet worden geannuleerd"
+ "cancel_inbound_failed": "Inkomende leveringen konden niet worden geannuleerd",
+ "seccomp_status": "Seccomp-BPF-sandbox (Linux)",
+ "seccomp_active": "Actief",
+ "seccomp_inactive": "Niet actief",
+ "seccomp_auto_enabled": "Automatisch ingeschakeld wanneer libseccomp beschikbaar is",
+ "seccomp_kernel_unsupported": "Seccomp-BPF niet beschikbaar (installeer libseccomp of platform niet ondersteund)",
+ "seccomp_disabled_by_env": "Uitgeschakeld via MESHCHAT_SECCOMP=0"
},
"common": {
"open": "Openen",
diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json
index bf40429e..d87ad58b 100644
--- a/meshchatx/src/frontend/locales/ru.json
+++ b/meshchatx/src/frontend/locales/ru.json
@@ -515,7 +515,13 @@
"cancel_inbound_deliveries_count": "Отменить входящие ({count})",
"cancel_inbound_confirm": "Отменить {count} входящую LXMF-передачу(и)? Крупные сообщения, которые сейчас загружаются, будут остановлены.",
"cancel_inbound_done": "Отменено входящих передач: {count}.",
- "cancel_inbound_failed": "Не удалось отменить входящие доставки"
+ "cancel_inbound_failed": "Не удалось отменить входящие доставки",
+ "seccomp_status": "Песочница Seccomp-BPF (Linux)",
+ "seccomp_active": "Активна",
+ "seccomp_inactive": "Не активна",
+ "seccomp_auto_enabled": "Включается автоматически при наличии libseccomp",
+ "seccomp_kernel_unsupported": "Seccomp-BPF недоступен (установите libseccomp или платформа не поддерживается)",
+ "seccomp_disabled_by_env": "Отключено через MESHCHAT_SECCOMP=0"
},
"common": {
"open": "Открыть",
diff --git a/meshchatx/src/frontend/locales/zh.json b/meshchatx/src/frontend/locales/zh.json
index 965a31d6..86551278 100644
--- a/meshchatx/src/frontend/locales/zh.json
+++ b/meshchatx/src/frontend/locales/zh.json
@@ -515,7 +515,13 @@
"cancel_inbound_deliveries_count": "取消传入({count})",
"cancel_inbound_confirm": "取消 {count} 个传入 LXMF 传输?正在下载的大消息将停止。",
"cancel_inbound_done": "已取消 {count} 个传入传输。",
- "cancel_inbound_failed": "无法取消传入投递"
+ "cancel_inbound_failed": "无法取消传入投递",
+ "seccomp_status": "Seccomp-BPF 沙箱(Linux)",
+ "seccomp_active": "已启用",
+ "seccomp_inactive": "未启用",
+ "seccomp_auto_enabled": "在提供 libseccomp 时自动启用",
+ "seccomp_kernel_unsupported": "Seccomp-BPF 不可用(请安装 libseccomp,或平台不支持)",
+ "seccomp_disabled_by_env": "已通过 MESHCHAT_SECCOMP=0 禁用"
},
"common": {
"open": "打开",
diff --git a/tests/backend/api_json_contract_schemas.py b/tests/backend/api_json_contract_schemas.py
index 5b01a3ad..0ff90e40 100644
--- a/tests/backend/api_json_contract_schemas.py
+++ b/tests/backend/api_json_contract_schemas.py
@@ -160,6 +160,11 @@ APP_INFO_BODY_SCHEMA: dict = {
"landlock_auto_enabled": {"type": "boolean"},
"landlock_disabled_by_env": {"type": "boolean"},
"landlock_active": {"type": "boolean"},
+ "seccomp_kernel_supported": {"type": "boolean"},
+ "seccomp_requested": {"type": "boolean"},
+ "seccomp_auto_enabled": {"type": "boolean"},
+ "seccomp_disabled_by_env": {"type": "boolean"},
+ "seccomp_active": {"type": "boolean"},
},
"additionalProperties": True,
}
@@ -175,6 +180,11 @@ _SERVER_BIND_STATUS_SCHEMA: dict = {
"landlock_auto_enabled": {"type": "boolean"},
"landlock_disabled_by_env": {"type": "boolean"},
"landlock_active": {"type": "boolean"},
+ "seccomp_kernel_supported": {"type": "boolean"},
+ "seccomp_requested": {"type": "boolean"},
+ "seccomp_auto_enabled": {"type": "boolean"},
+ "seccomp_disabled_by_env": {"type": "boolean"},
+ "seccomp_active": {"type": "boolean"},
}
API_V1_STATUS_SCHEMA: dict = {
diff --git a/tests/backend/test_api_json_contracts.py b/tests/backend/test_api_json_contracts.py
index ec0413c3..be19c66c 100644
--- a/tests/backend/test_api_json_contracts.py
+++ b/tests/backend/test_api_json_contracts.py
@@ -175,6 +175,11 @@ def test_status_schema_accepts_starting_and_failed_envelopes():
"landlock_auto_enabled": False,
"landlock_disabled_by_env": False,
"landlock_active": False,
+ "seccomp_kernel_supported": False,
+ "seccomp_requested": False,
+ "seccomp_auto_enabled": False,
+ "seccomp_disabled_by_env": False,
+ "seccomp_active": False,
}
assert_matches_schema(starting, API_V1_STATUS_SCHEMA)
failed = {
diff --git a/tests/backend/test_deferred_network_startup.py b/tests/backend/test_deferred_network_startup.py
index a1c17109..8a97dd13 100644
--- a/tests/backend/test_deferred_network_startup.py
+++ b/tests/backend/test_deferred_network_startup.py
@@ -398,6 +398,11 @@ def test_status_schema_fuzz_valid_envelopes(status, stage, network_ready, error)
"landlock_auto_enabled": False,
"landlock_disabled_by_env": False,
"landlock_active": False,
+ "seccomp_kernel_supported": False,
+ "seccomp_requested": False,
+ "seccomp_auto_enabled": False,
+ "seccomp_disabled_by_env": False,
+ "seccomp_active": False,
}
if error is not None:
sample["error"] = error
diff --git a/tests/backend/test_seccomp_sandbox.py b/tests/backend/test_seccomp_sandbox.py
new file mode 100644
index 00000000..6bac56c5
--- /dev/null
+++ b/tests/backend/test_seccomp_sandbox.py
@@ -0,0 +1,122 @@
+# SPDX-License-Identifier: 0BSD
+
+"""Tests for optional seccomp-BPF sandbox."""
+
+import sys
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from meshchatx.src.backend import seccomp_sandbox as sc
+
+
+@pytest.fixture(autouse=True)
+def _reset_seccomp_caches():
+ sc._seccomp_support_cached = None
+ sc._seccomp_lib_cached = None
+ sc._seccomp_lib_failed = False
+ yield
+ sc._seccomp_support_cached = None
+ sc._seccomp_lib_cached = None
+ sc._seccomp_lib_failed = False
+
+
+def test_seccomp_requested_non_linux():
+ with (
+ patch.object(sc.sys, "platform", "darwin"),
+ patch.object(sc, "_is_android", return_value=False),
+ ):
+ assert sc.seccomp_requested() is False
+
+
+def test_seccomp_requested_respects_disable_env(monkeypatch):
+ monkeypatch.setenv("MESHCHAT_SECCOMP", "0")
+ with (
+ patch.object(sc.sys, "platform", "linux"),
+ patch.object(sc, "_is_android", return_value=False),
+ ):
+ assert sc.seccomp_requested() is False
+ assert sc.seccomp_disabled_by_env() is True
+
+
+def test_seccomp_requested_force_enable_env(monkeypatch):
+ monkeypatch.setenv("MESHCHAT_SECCOMP", "1")
+ with (
+ patch.object(sc.sys, "platform", "linux"),
+ patch.object(sc, "_is_android", return_value=False),
+ ):
+ assert sc.seccomp_requested() is True
+ assert sc.seccomp_auto_enabled() is False
+
+
+def test_seccomp_auto_when_supported(monkeypatch):
+ monkeypatch.delenv("MESHCHAT_SECCOMP", raising=False)
+ with (
+ patch.object(sc.sys, "platform", "linux"),
+ patch.object(sc, "_is_android", return_value=False),
+ patch.object(sc, "seccomp_kernel_supported", return_value=True),
+ ):
+ assert sc.seccomp_requested() is True
+ assert sc.seccomp_auto_enabled() is True
+
+
+def test_seccomp_auto_off_when_unsupported(monkeypatch):
+ monkeypatch.delenv("MESHCHAT_SECCOMP", raising=False)
+ with (
+ patch.object(sc.sys, "platform", "linux"),
+ patch.object(sc, "_is_android", return_value=False),
+ patch.object(sc, "seccomp_kernel_supported", return_value=False),
+ ):
+ assert sc.seccomp_requested() is False
+
+
+def test_seccomp_kernel_supported_false_on_android():
+ with patch.object(sc, "_is_android", return_value=True):
+ assert sc.seccomp_kernel_supported() is False
+
+
+def test_apply_seccomp_falls_back_when_not_requested(monkeypatch):
+ monkeypatch.setenv("MESHCHAT_SECCOMP", "0")
+ assert sc.apply_seccomp_sandbox() is False
+
+
+def test_apply_seccomp_falls_back_without_libseccomp(monkeypatch):
+ monkeypatch.setenv("MESHCHAT_SECCOMP", "1")
+ with (
+ patch.object(sc.sys, "platform", "linux"),
+ patch.object(sc, "_is_android", return_value=False),
+ patch.object(sc, "_load_libseccomp", return_value=None),
+ ):
+ assert sc.apply_seccomp_sandbox() is False
+
+
+def test_apply_seccomp_loads_denylist(monkeypatch):
+ monkeypatch.setenv("MESHCHAT_SECCOMP", "1")
+ lib = MagicMock()
+ ctx = object()
+ lib.seccomp_init.return_value = ctx
+ lib.seccomp_syscall_resolve_name.side_effect = lambda name: {
+ b"mount": 165,
+ b"ptrace": 101,
+ b"bpf": 321,
+ }.get(name, -1)
+ lib.seccomp_rule_add.return_value = 0
+ lib.seccomp_load.return_value = 0
+
+ with (
+ patch.object(sc.sys, "platform", "linux"),
+ patch.object(sc, "_is_android", return_value=False),
+ patch.object(sc, "_load_libseccomp", return_value=lib),
+ ):
+ assert sc.apply_seccomp_sandbox() is True
+
+ lib.seccomp_init.assert_called_once_with(sc._SCMP_ACT_ALLOW)
+ assert lib.seccomp_rule_add.call_count >= 3
+ lib.seccomp_load.assert_called_once_with(ctx)
+ lib.seccomp_release.assert_called_once_with(ctx)
+
+
+@pytest.mark.skipif(sys.platform != "linux", reason="seccomp probe requires Linux")
+def test_seccomp_kernel_supported_on_linux():
+ supported = sc.seccomp_kernel_supported()
+ assert isinstance(supported, bool)
diff --git a/tests/frontend/ToolsPage.test.js b/tests/frontend/ToolsPage.test.js
index 1f75845c..f7ef900a 100644
--- a/tests/frontend/ToolsPage.test.js
+++ b/tests/frontend/ToolsPage.test.js
@@ -83,6 +83,14 @@ describe("ToolsPage.vue", () => {
expect(rnshRow?.text()).toContain("tools.alpha_badge");
});
+ it("shows an alpha badge on the rns-filesync tool", () => {
+ const wrapper = mountToolsPage();
+ const filesync = wrapper.vm.tools.find((tool) => tool.name === "rns-filesync");
+ expect(filesync?.alpha).toBe(true);
+ const filesyncRow = wrapper.findAll(".tool-row").find((row) => row.text().includes("tools.rns_filesync.title"));
+ expect(filesyncRow?.text()).toContain("tools.alpha_badge");
+ });
+
it("clears search query when close button is clicked", async () => {
const wrapper = mountToolsPage();
const searchInput = wrapper.find("input");
diff --git a/tests/frontend/filesyncPackagingContracts.test.js b/tests/frontend/filesyncPackagingContracts.test.js
index 1e58c7ad..5cdece1d 100644
--- a/tests/frontend/filesyncPackagingContracts.test.js
+++ b/tests/frontend/filesyncPackagingContracts.test.js
@@ -28,7 +28,8 @@ describe("filesync packaging contracts", () => {
const tools = readSource("meshchatx/src/frontend/js/registries/coreToolsEntries.js");
expect(tools).toContain('name: "rns-filesync"');
expect(tools).toContain('route: { name: "rns-filesync" }');
- const block = tools.slice(tools.indexOf('name: "rns-filesync"'), tools.indexOf('name: "debug-logs"'));
+ const block = tools.slice(tools.indexOf('name: "rns-filesync"'), tools.indexOf('name: "rnsh"'));
expect(block).not.toContain("comingSoon");
+ expect(block).toContain("alpha: true");
});
});
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────